﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Runtime.InteropServices;
using System.IO;

namespace WpfKlip.Core.Win
{

    using Bitmap = System.Drawing.Bitmap;
    /// <summary>
    /// Summary description for ThumbnailCreator.
    /// http://www.vbaccelerator.com/home/net/code/libraries/shell_projects/Thumbnail_Extraction/article.asp
    /// </summary>
    class ShellThumbnailsService
    {
        internal static Bitmap GetThumbOrIcon(string path)
        {
            ShellThumbnailsService ShellThumbnailsService = new ShellThumbnailsService();
            try
            {
                var thumb = ShellThumbnailsService.GetThumbNail(path);
                return thumb;
            }
            catch
            {
                /*if (Environment.OSVersion.Version.Major > 5)
                    return FileToIconConverter.GetFileIcon(path, FileToIconConverter.IconSize.jumbo).ToBitmap();
                else
                    return FileToIconConverter.GetFileIcon(path, FileToIconConverter.IconSize.extraLarge).ToBitmap();*/

                return null;
            }
        }

        #region ShellFolder Enumerations
		[Flags]
		private enum ESTRRET : int
		{
			STRRET_WSTR     = 0x0000,			// Use STRRET.pOleStr
			STRRET_OFFSET   = 0x0001,			// Use STRRET.uOffset to Ansi
			STRRET_CSTR     = 0x0002			// Use STRRET.cStr
		}
		[Flags]
		private enum ESHCONTF : int
		{
			SHCONTF_FOLDERS = 32,
			SHCONTF_NONFOLDERS = 64,
			SHCONTF_INCLUDEHIDDEN = 128
		}

		[Flags]
		private enum ESHGDN : int
		{
			SHGDN_NORMAL = 0,
			SHGDN_INFOLDER = 1,
			SHGDN_FORADDRESSBAR = 16384,
			SHGDN_FORPARSING = 32768
		}
		[Flags]
		private enum ESFGAO : int
		{
			SFGAO_CANCOPY = 1,
			SFGAO_CANMOVE = 2,
			SFGAO_CANLINK = 4,
			SFGAO_CANRENAME = 16,
			SFGAO_CANDELETE = 32,
			SFGAO_HASPROPSHEET = 64,
			SFGAO_DROPTARGET = 256,
			SFGAO_CAPABILITYMASK = 375,
			SFGAO_LINK = 65536,
			SFGAO_SHARE = 131072,
			SFGAO_READONLY = 262144,
			SFGAO_GHOSTED = 524288,
			SFGAO_DISPLAYATTRMASK = 983040,
			SFGAO_FILESYSANCESTOR = 268435456,
			SFGAO_FOLDER = 536870912,
			SFGAO_FILESYSTEM = 1073741824,
			SFGAO_HASSUBFOLDER = -2147483648,
			SFGAO_CONTENTSMASK = -2147483648,
			SFGAO_VALIDATE = 16777216,
			SFGAO_REMOVABLE = 33554432,
			SFGAO_COMPRESSED = 67108864
		}
		#endregion

		#region IExtractImage Enumerations
		private enum EIEIFLAG
		{
			IEIFLAG_ASYNC       = 0x0001,      // ask the extractor if it supports ASYNC extract (free threaded)
			IEIFLAG_CACHE       = 0x0002,      // returned from the extractor if it does NOT cache the thumbnail
			IEIFLAG_ASPECT      = 0x0004,      // passed to the extractor to beg it to render to the aspect ratio of the supplied rect
			IEIFLAG_OFFLINE     = 0x0008,      // if the extractor shouldn't hit the net to get any content neede for the rendering
			IEIFLAG_GLEAM       = 0x0010,     // does the image have a gleam ? this will be returned if it does
			IEIFLAG_SCREEN      = 0x0020,      // render as if for the screen  (this is exlusive with IEIFLAG_ASPECT )
			IEIFLAG_ORIGSIZE    = 0x0040,      // render to the approx size passed, but crop if neccessary
			IEIFLAG_NOSTAMP     = 0x0080,      // returned from the extractor if it does NOT want an icon stamp on the thumbnail
			IEIFLAG_NOBORDER    = 0x0100,      // returned from the extractor if it does NOT want an a border around the thumbnail
			IEIFLAG_QUALITY     = 0x0200      // passed to the Extract method to indicate that a slower, higher quality image is desired, re-compute the thumbnail
		}
		#endregion

		#region ShellFolder Structures 
		[StructLayoutAttribute(LayoutKind.Sequential, Pack=4, Size=0, CharSet=CharSet.Auto)]
		private struct STRRET_CSTR
		{
			public ESTRRET uType;
			[MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst=520)]
			public byte[]			cStr;
		}
	
		[StructLayout(LayoutKind.Explicit, CharSet=CharSet.Auto)]
		private struct STRRET_ANY
		{
			[FieldOffset(0)] 
			public ESTRRET uType;
			[FieldOffset(4)] 
			public IntPtr pOLEString; 
		}

		[StructLayoutAttribute(LayoutKind.Sequential)]
		private struct SIZE 
		{
			public int cx;
			public int cy;
		}
		#endregion

		#region Com Interop for IUnknown
		[ComImport, Guid("00000000-0000-0000-C000-000000000046")]
		[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
		private interface IUnknown
		{
			[PreserveSig]
			IntPtr QueryInterface(ref Guid riid, out IntPtr pVoid);
		
			[PreserveSig]
			IntPtr AddRef();

			[PreserveSig]
			IntPtr Release();
		}
		#endregion

		#region COM Interop for IMalloc
		[ComImportAttribute()]
		[GuidAttribute("00000002-0000-0000-C000-000000000046")]
		[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
		//helpstring("IMalloc interface")
		private interface IMalloc
		{
			[PreserveSig]
			IntPtr Alloc(int cb);

			[PreserveSig]
			IntPtr Realloc(
				IntPtr pv,
				int cb);
		
			[PreserveSig]
			void Free(IntPtr pv);

			[PreserveSig]
			int GetSize(IntPtr pv);

			[PreserveSig]
			int DidAlloc(IntPtr pv);

			[PreserveSig]
			void  HeapMinimize();
		};
		#endregion

		#region COM Interop for IEnumIDList
		[ComImportAttribute()]
		[GuidAttribute("000214F2-0000-0000-C000-000000000046")]
		[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
		//helpstring("IEnumIDList interface")
		private interface IEnumIDList
		{
			[PreserveSig]
			int Next(
				int celt, 
				ref IntPtr rgelt, 
				out int pceltFetched);

			void Skip(
				int celt);

			void Reset();
    
			void Clone(
				ref IEnumIDList ppenum);
		};
		#endregion

		#region COM Interop for IShellFolder
		[ComImportAttribute()]
		[GuidAttribute("000214E6-0000-0000-C000-000000000046")]
		[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
		//helpstring("IShellFolder interface")
		private interface IShellFolder
		{
			void ParseDisplayName(
				IntPtr hwndOwner, 
				IntPtr pbcReserved, 
				[MarshalAs(UnmanagedType.LPWStr)] string lpszDisplayName,
				out int pchEaten, 
				out IntPtr ppidl, 
				out int pdwAttributes);

			void EnumObjects(
				IntPtr hwndOwner, 
				[MarshalAs(UnmanagedType.U4)] ESHCONTF grfFlags,
				ref IEnumIDList ppenumIDList
				);

			void BindToObject(
				IntPtr pidl, 
				IntPtr pbcReserved, 
				ref Guid riid, 
				ref IShellFolder ppvOut);

			void BindToStorage(
				IntPtr pidl, 
				IntPtr pbcReserved, 
				ref Guid riid, 
				IntPtr ppvObj
				);

			[PreserveSig]
			int CompareIDs(
				IntPtr lParam, 
				IntPtr pidl1, 
				IntPtr pidl2);

			void CreateViewObject(
				IntPtr hwndOwner, 
				ref Guid riid, 
				IntPtr ppvOut);

			void GetAttributesOf(
				int cidl, 
				IntPtr apidl, 
				[MarshalAs(UnmanagedType.U4)] ref ESFGAO rgfInOut); 

			void GetUIObjectOf(
				IntPtr hwndOwner, 
				int cidl, 
				ref IntPtr apidl, 
				ref Guid riid, 
				out int prgfInOut, 
				ref IUnknown ppvOut);

			void GetDisplayNameOf(
				IntPtr pidl, 
				[MarshalAs(UnmanagedType.U4)] ESHGDN uFlags,  
				ref STRRET_CSTR lpName);

			void SetNameOf(
				IntPtr hwndOwner, 
				IntPtr pidl,
				[MarshalAs(UnmanagedType.LPWStr)] string lpszName,
				[MarshalAs(UnmanagedType.U4)] ESHCONTF uFlags,
				ref IntPtr ppidlOut);
		};
		#endregion

		#region COM Interop for IExtractImage
		[ComImportAttribute()]
		[GuidAttribute("BB2E617C-0920-11d1-9A0B-00C04FC2D6C1")]
		[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
		//helpstring("IExtractImage"),
		private interface IExtractImage
		{
			void GetLocation(
				[Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszPathBuffer,
				int cch,
				ref int pdwPriority,
				ref SIZE prgSize,
				int dwRecClrDepth,
				ref int pdwFlags);

			void Extract(
				out IntPtr phBmpThumbnail);
		}
		#endregion
	
		#region UnManagedMethods for IShellFolder
		private class UnManagedMethods
		{
			[DllImport("shell32", CharSet = CharSet.Auto)]
			internal extern static int SHGetMalloc(out IMalloc ppMalloc);

			[DllImport("shell32", CharSet = CharSet.Auto)]
			internal extern static int SHGetDesktopFolder(out IShellFolder ppshf);
		
			[DllImport("shell32", CharSet = CharSet.Auto)]
			internal extern static int SHGetPathFromIDList (
				IntPtr pidl, 
				StringBuilder pszPath);

			[DllImport("gdi32", CharSet = CharSet.Auto)]
			internal extern static int DeleteObject (
				IntPtr hObject
				);
		}	
		#endregion

		#region Member Variables
		private IMalloc alloc = null;
		private bool disposed = false;
		private System.Drawing.Size desiredSize = new System.Drawing.Size(100,100);
		private System.Drawing.Bitmap thumbNail = null;
		#endregion

		#region Implementation
		public System.Drawing.Bitmap ThumbNail
		{
			get
			{
				return thumbNail;
			}
		}


		public System.Drawing.Size DesiredSize
		{
			get
			{
				return desiredSize;
			}
			set
			{
				desiredSize = value;
			}
		}

		private IMalloc Allocator
		{
			get
			{
				if (!disposed)
				{
					if (alloc == null)
					{
						UnManagedMethods.SHGetMalloc(out alloc);					
					}
				}
				else
				{
					System.Diagnostics.Debug.Assert(false, "Object has been disposed.");
				}
				return alloc;
			}
		}

		public System.Drawing.Bitmap GetThumbNail(
			string file
			)
		{
			if ((!File.Exists(file)) && (!Directory.Exists(file)))
			{
				throw new FileNotFoundException(
					String.Format("The file '{0}' does not exist", file),
					file);
			}

			if (thumbNail != null)
			{
				thumbNail.Dispose();
				thumbNail = null;
			}

			IShellFolder folder = null;
			try
			{
				folder = getDesktopFolder;
			}
			catch (Exception ex)
			{
				throw ex;
			}

			if (folder != null)
			{
				IntPtr pidlMain = IntPtr.Zero;
				try
				{
					int cParsed = 0;
					int pdwAttrib = 0;
					string filePath = Path.GetDirectoryName(file);
					pidlMain = IntPtr.Zero;
					folder.ParseDisplayName(
						IntPtr.Zero, 
						IntPtr.Zero, 
						filePath, 
						out cParsed, 
						out pidlMain,
						out pdwAttrib);
				}
				catch (Exception ex)
				{
					Marshal.ReleaseComObject(folder);
					throw ex;
				}

				if (pidlMain != IntPtr.Zero)
				{
					// IShellFolder:
					Guid iidShellFolder = new Guid("000214E6-0000-0000-C000-000000000046");					
					IShellFolder item = null;

					try
					{
						folder.BindToObject(pidlMain, IntPtr.Zero, ref iidShellFolder, ref item);
					}
					catch (Exception ex)
					{
						Marshal.ReleaseComObject(folder);
						Allocator.Free(pidlMain);
						throw ex;
					}

					if (item != null)
					{
						//
						IEnumIDList idEnum = null;
						try
						{
							item.EnumObjects(
								IntPtr.Zero, 
								(ESHCONTF.SHCONTF_FOLDERS | ESHCONTF.SHCONTF_NONFOLDERS),
								ref idEnum);
						}
						catch (Exception ex)
						{
							Marshal.ReleaseComObject(folder);
							Allocator.Free(pidlMain);
							throw ex;
						}
						
						if (idEnum != null)
						{
							// start reading the enum:
							int hRes = 0;
							IntPtr pidl = IntPtr.Zero;
							int fetched = 0;
							bool complete = false;
							while (!complete)
							{
								hRes = idEnum.Next(1, ref pidl, out fetched);
								if (hRes != 0)
								{
									pidl = IntPtr.Zero;
									complete = true;
								}
								else
								{
									if (getThumbNail(file, pidl, item))
									{
										complete = true;
									}
								}
								if (pidl != IntPtr.Zero)
								{
									Allocator.Free(pidl);
								}
							}

							Marshal.ReleaseComObject(idEnum);
						}


						Marshal.ReleaseComObject(item);
					}

					Allocator.Free(pidlMain);
				}

				Marshal.ReleaseComObject(folder);
			}
			return thumbNail;
		}

		private bool getThumbNail(
			string file,
			IntPtr pidl, 
			IShellFolder item
			)
		{
			IntPtr hBmp = IntPtr.Zero;
			IExtractImage extractImage = null;

			try
			{
				string pidlPath = PathFromPidl(pidl);
				if (Path.GetFileName(pidlPath).ToUpper().Equals(Path.GetFileName(file).ToUpper()))
				{
					// we have the item:								
					IUnknown iunk = null;
					int prgf = 0;
					Guid iidExtractImage = new Guid("BB2E617C-0920-11d1-9A0B-00C04FC2D6C1");
					item.GetUIObjectOf(
						IntPtr.Zero, 
						1, 
						ref pidl, 
						ref iidExtractImage,
						out prgf,
						ref iunk);				
					extractImage = (IExtractImage)iunk;
					
					if (extractImage != null)
					{
						Console.WriteLine("Got an IExtractImage object!");
						SIZE sz = new SIZE();
						sz.cx = desiredSize.Width;
						sz.cy = desiredSize.Height;
						StringBuilder location = new StringBuilder(260, 260);
						int priority = 0;
						int requestedColourDepth = 32;
						EIEIFLAG flags = EIEIFLAG.IEIFLAG_ASPECT | EIEIFLAG.IEIFLAG_SCREEN;
						int uFlags = (int)flags;
						
						extractImage.GetLocation(
							location, 
							location.Capacity, 
							ref priority,
							ref sz,
							requestedColourDepth,
							ref uFlags);

						extractImage.Extract(out hBmp);						
						if (hBmp != IntPtr.Zero)
						{
							// create the image object:
							thumbNail = System.Drawing.Bitmap.FromHbitmap(hBmp);
							// is thumbNail owned by the Bitmap?
						}

						Marshal.ReleaseComObject(extractImage);
						extractImage = null;
					}
					return true;
				}
				else
				{
					return false;
				}
			}
			catch (Exception ex)
			{
				if (hBmp != IntPtr.Zero)
				{
					UnManagedMethods.DeleteObject(hBmp);
				}
				if (extractImage != null)
				{
					Marshal.ReleaseComObject(extractImage);
				}
				throw ex;
			}
		}

		private string PathFromPidl(
			IntPtr pidl
			)
		{
			StringBuilder path = new StringBuilder(260, 260);
			int result = UnManagedMethods.SHGetPathFromIDList(pidl, path);
			if (result == 0)
			{
				return string.Empty;
			}
			else
			{
				return path.ToString();	
			}
		}
	
		private IShellFolder getDesktopFolder
		{
			get
			{
				IShellFolder ppshf;
				int r = UnManagedMethods.SHGetDesktopFolder(out ppshf);
				return ppshf;
			}
		}
		#endregion

		#region Constructor, Destructor, Dispose							 
		public ShellThumbnailsService()
		{
		}

		public void Dispose()
		{
			if (!disposed)
			{
				if (alloc != null)
				{
					Marshal.ReleaseComObject(alloc);
				}
				alloc = null;

				if (thumbNail != null)
				{
					thumbNail.Dispose();
				}

				disposed = true;
			}
		}

        ~ShellThumbnailsService()
		{
			Dispose();
		}
		#endregion
    }
}
